구글 코랩에서 실행하기
|
2. Data Preparation#
본 챕터에서는 모빌리티 시뮬레이션을 구축하는데 필요한 기본적인 입력데이터들을 살펴본다. 단순히 데이터를 살펴보는데서 끝나지 않고 간단한 데이터의 전처리 및 시각화의 실습과정도 포함하고 있다.
매우 다양한 형태의 모빌리티 데이터가 존재하며, 이들을 분류하는 기준 역시 분석자의 목표에 따라 여려가지로 구분할 수 있지만 본 책에서는 크게 아래와 같은 형태의 데이터를 살펴볼 것이다.
통행수요 데이터 (Travel Demand)
시뮬레이션을 하기 위한 승객의 수요를 생성하는 기초자료로 활용됨
통행시간 데이터 (Travel Time)
시뮬레이션을 Calibration 하는데 활용
e.g., 출퇴근 시간의 교통 정체, 새벽시간대의 빠른 속도를 시뮬레이션에 반영 가능함
도로 네트워크 데이터 (Road Network)
도로 위를 움직이는 차량 구현 (Vehicle Router)
대중교통 네트워크 데이터 (Public Transit Network)
버스, 지하철과 같은 노선과 배차간격이 정해진 모빌리티 시스템을 구현하는데 사용
2.1 Travel Demand Data (Trip record data)#
2.1.1 활용 데이터#
통행수요는 특정지역에서 출발(Trip Production)하거나, 특정지역으로 도착하는 통행(Trip Attraction) 형태로 존재할 수도 있고, 혹은 더 세분화 한다면 출발지-목적지 단위의 통행수요(Origin-Destination (OD) Travel Demand)로 나타낼 수 있다.
최근 많은 사람들이 스마트폰을 사용하고 따라서 통신사 기지국 기반으로 세밀한 통행패턴이 수집 가능하며, O-D 단위의 통행수요 데이터를 구득하기가 수월해졌다. 따라서 본 교재에서는 O-D 단위의 통행 데이터를 주로 활용하는 실습을 진행할 예정이다.
본 예제에서는 서울시에서 제공하는 수도권 생활이동 데이터를 활용하였다.
2.1.2 데이터 읽기 및 간단한 전처리#
간단한 예제를 위해 서울시에서 제공하는 수도권 생활이동 데이터 중, 2024년 3월 27일 하루치의 데이터만을 예제로 사용하였다.
원활한 실습을 위해 모든 데이터는 Google Drive에 업로드 하였으며,
gdown패키지를 사용해 데이터를 다운로드 받은 후 로드하는 방식을 사용하였다.
import gdown
import pandas as pd
import numpy as np
import os
import topojson as tp # 공간정보를 간소화 해주는 패키지 (분석속도 & 결과물 용량에 영향을 미침)
import geopandas as gpd
import pydeck as pdk
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[1], line 1
----> 1 import gdown
2 import pandas as pd
3 import numpy as np
ModuleNotFoundError: No module named 'gdown'
def download_and_read_parquet(file_id, output_path="../data/chp2_od_data.parquet"):
try:
# Google Drive에서 파일 다운로드
gdown.download(id=file_id, output=output_path, quiet=False)
# Parquet 파일을 DataFrame으로 읽기
df = pd.read_parquet(output_path)
# 임시 파일 삭제 (데이터 용량이 매우 큰 경우 사용)
# os.remove(output_path) # 다운로드 받은 데이터를 삭제하고 싶지 않을 때는 해당 라인을 주석처리
return df
except Exception as e:
print(f"오류 발생: {e}")
return None
# 파일 불러오기
file_id = "1uJX9MuNX2J6J5SpE-RQy05XPsae6Fg4x" # 구글 드라이브에 업로드 된 파일의 ID
df = download_and_read_parquet(file_id)
Downloading...
From (original): https://drive.google.com/uc?id=1uJX9MuNX2J6J5SpE-RQy05XPsae6Fg4x
From (redirected): https://drive.google.com/uc?id=1uJX9MuNX2J6J5SpE-RQy05XPsae6Fg4x&confirm=t&uuid=7c140fbc-f5ac-4c46-84a0-b776c324994b
To: f:\mobility-simulation-book\data\chp2_od_data.parquet
100%|██████████| 56.6M/56.6M [00:01<00:00, 40.9MB/s]
인터넷 환경으로 인해 파일을 불러오는게 느리다면, github에서 clone 할 때 가지고 온 data 폴더에 있는 파일을 직접 열 수 있다.
아래 코드를 실행해 보자.
df = pd.read_parquet("../data/chp2_od_data.parquet")
df
| O_ADMDONG_CD | D_ADMDONG_CD | ST_TIME_CD | CNT | MOVE_DIST | MOVE_TIME | |
|---|---|---|---|---|---|---|
| 0 | 11110515 | 11110515 | 0 | 25.54 | 151.875714 | 1683.637143 |
| 1 | 11110515 | 11110515 | 1 | 79.29 | 104.120000 | 1210.596667 |
| 2 | 11110515 | 11110515 | 2 | 103.07 | 175.205714 | 360.190000 |
| 3 | 11110515 | 11110515 | 3 | 16.30 | 239.946667 | 2441.596667 |
| 4 | 11110515 | 11110515 | 4 | 34.73 | 215.720000 | 860.990000 |
| ... | ... | ... | ... | ... | ... | ... |
| 3792913 | 52800410 | 41590360 | 11 | 2.56 | 69512.350000 | 4087.830000 |
| 3792914 | 52800420 | 11350570 | 14 | 3.50 | 66976.630000 | 6354.770000 |
| 3792915 | 52800420 | 11440630 | 9 | 3.48 | 63894.260000 | 3725.740000 |
| 3792916 | 52800420 | 41273610 | 17 | 2.33 | 69839.840000 | 1864.100000 |
| 3792917 | 52800420 | 41590253 | 15 | 3.72 | 50118.480000 | 3850.300000 |
3792918 rows × 6 columns
컬럼의 명세는 아래와 같다. 행정동 단위의 O-D 통행량 및 통행시간 정보를 제공해주고 있다. 더 상세하게는 내/외국인 구분, 국적, 이동목적과 같은 정보도 제공을 해준다.
순번 |
영문 컬럼명 |
컬럼 설명 |
NULL 여부 |
NULL 대체값 |
형식 |
규칙 |
데이터 허용범위 |
비고 |
|---|---|---|---|---|---|---|---|---|
1 |
O_ADMDONG_CD |
출발 행정동 |
X |
- |
STRING |
- |
- |
행안부 8자리 코드체계 |
2 |
D_ADMDONG_CD |
도착 행정동 |
X |
- |
STRING |
- |
- |
행안부 8자리 코드체계 |
3 |
ST_TIME_CD |
출발 시간 |
X |
- |
STRING |
- |
- |
7-9시/17시-19시는 20분단위, 그 외 1시간 단위 |
4 |
FNS_TIME_CD |
도착 시간 |
X |
- |
STRING |
- |
- |
7-9시/17시-19시는 20분단위, 그 외 1시간 단위 |
5 |
IN_FORN_DIV_NM |
내/외국인 구분 |
X |
- |
STRING |
- |
- |
내국인, 단기외국인, 장기외국인 |
6 |
FORN_CITIZ_NM |
국적 |
X |
- |
STRING |
- |
- |
- |
7 |
MOVE_PURPOSE |
이동 목적 |
X |
- |
STRING |
- |
- |
1: 출근, 2 : 등교, 3: 귀가, 4: 쇼핑, 5: 관광, 6: 병원, 7: 기타 |
8 |
MOVE_DIST |
평균 이동 거리(m) |
X |
- |
DOUBLE |
- |
- |
- |
9 |
MOVE_TIME |
평균 이동 시간(분) |
X |
- |
DOUBLE |
- |
- |
- |
10 |
CNT |
이동인구 수 |
X |
- |
DOUBLE |
(18,2) |
- |
- |
11 |
ETL_YMD |
기준 년월 일 |
X |
- |
STRING |
yyyyMMdd |
데이터 기준 당일 |
- |
2.1.3 Basic visualization#
통행량 데이터의 경우 출발지/도착지에 대한 공간정보 및 통행시간 정보가 포함된 데이터이다.
도시/교통 분야의 데이터는 이처럼 시간과 공간의 정보를 동시에 가지고 있는 ‘시공간 데이터’인 경우가 많으며 이를 분석하기 위한 기초적인 분석기술 및 핵심 패키지를 이해하는 것이 중요하다.
공간데이터를 분석하기 위한 Python 패키지로는 대표적으로
geopandas가 있으며, 시각화를 위한 패키지로는folium,plotly,pydeck이 있다.위의 키워드로하여 인터넷 검색을 해보면 다양한 튜토리얼 및 예제 코드가 있으므로 시간을 할애해서 공부하는 것을 추천하며, 본 책에서는 상세히 다루지 않는다.
아래는 내가 추천하는 시간을 할애해서 공부하면 좋은 자료들이다.
수도권 생활이동 데이터의 경우 행정동 단위로 데이터가 존재하기 때문에 공간상에 시각화를 하기 위해서는 행정동의 geometry 정보를 담고 있는 데이터가 추가로 필요하다.
본 실습에서는 vuski/admdongkor 에서제공하는 행정동 경계 데이터를 활용하여 간단한 시각화를 수행한다
def download_and_read_geojson(file_id, output_path="../data/chp2_HangJeongDong_ver20230101.geojson"):
try:
# Google Drive에서 파일 다운로드
gdown.download(id=file_id, output=output_path, quiet=False)
# Parquet 파일을 DataFrame으로 읽기
df = gpd.read_file(output_path, driver='GeoJSON')
# 임시 파일 삭제 (데이터 용량이 매우 큰 경우 사용)
# os.remove(output_path) # 다운로드 받은 데이터를 삭제하고 싶지 않을 때는 해당 라인을 주석처리
return df
except Exception as e:
print(f"오류 발생: {e}")
return None
#### 행정경계 데이터 불러오기
file_id = "1u8V4h-yUef0g-RJ445wsBgHXpMyUj7ih" # 구글 드라이브에 업로드 된 파일의 ID
gdf_adm = download_and_read_geojson(file_id)
# gdf_adm = gpd.read_file('../data/chp2_HangJeongDong_ver20230101.geojson', driver='GeoJSON')
Downloading...
From: https://drive.google.com/uc?id=1u8V4h-yUef0g-RJ445wsBgHXpMyUj7ih
To: f:\mobility-simulation-book\data\chp2_HangJeongDong_ver20230101.geojson
100%|██████████| 34.9M/34.9M [00:00<00:00, 35.0MB/s]
#### 서울, 경기, 인천 지역만 추출
gdf_adm = gdf_adm[gdf_adm['sidonm'].isin(['서울특별시', '경기도', '인천광역시'])].reset_index(drop=True)
#### 유동인구와 일치하는 행정동코드 컬럼(`adm_cd2`)을 숫자형태로 변환
gdf_adm['adm_cd2'] = pd.to_numeric(gdf_adm['adm_cd2'])
#### Divide by 100 and create the new column 'ADMDONG_CD'
gdf_adm['ADMDONG_CD'] = gdf_adm['adm_cd2'] / 100
#### Convert the 'ADMDONG_CD' column to integer
gdf_adm['ADMDONG_CD'] = (gdf_adm['ADMDONG_CD']).astype(int)
#### name, sgg 컬럼만 남기기
gdf_adm = gdf_adm[['adm_nm','ADMDONG_CD','geometry']].copy()
gdf_adm
| adm_nm | ADMDONG_CD | geometry | |
|---|---|---|---|
| 0 | 서울특별시 종로구 사직동 | 11110530 | MULTIPOLYGON (((126.97689 37.57565, 126.97703 ... |
| 1 | 서울특별시 종로구 삼청동 | 11110540 | MULTIPOLYGON (((126.98269 37.59507, 126.98337 ... |
| 2 | 서울특별시 종로구 부암동 | 11110550 | MULTIPOLYGON (((126.97585 37.59656, 126.97359 ... |
| 3 | 서울특별시 종로구 평창동 | 11110560 | MULTIPOLYGON (((126.97507 37.63139, 126.97649 ... |
| 4 | 서울특별시 종로구 무악동 | 11110570 | MULTIPOLYGON (((126.96067 37.58080, 126.96281 ... |
| ... | ... | ... | ... |
| 1140 | 경기도 고양시일산서구 탄현2동 | 41287546 | MULTIPOLYGON (((126.77625 37.69403, 126.77598 ... |
| 1141 | 경기도 고양시일산서구 덕이동 | 41287600 | MULTIPOLYGON (((126.72908 37.68191, 126.72906 ... |
| 1142 | 경기도 광주시 오포2동 | 41610590 | MULTIPOLYGON (((127.23876 37.35970, 127.23997 ... |
| 1143 | 경기도 광주시 오포1동 | 41610580 | MULTIPOLYGON (((127.19171 37.34718, 127.19221 ... |
| 1144 | 경기도 광주시 신현동 | 41610600 | MULTIPOLYGON (((127.18746 37.36485, 127.18707 ... |
1145 rows × 3 columns
gdf_adm.plot()
<Axes: >
#### 행정동 데이터 기하구조 간소화
simplified_geometry = tp.Topology(gdf_adm, toposimplify=.005).to_gdf()
simplified_geometry.plot()
<Axes: >
# Interactive Map
simplified_geometry.explore(tiles = 'CartoDB positron')
df
| O_ADMDONG_CD | D_ADMDONG_CD | ST_TIME_CD | CNT | MOVE_DIST | MOVE_TIME | |
|---|---|---|---|---|---|---|
| 0 | 11110515 | 11110515 | 0 | 25.54 | 151.875714 | 1683.637143 |
| 1 | 11110515 | 11110515 | 1 | 79.29 | 104.120000 | 1210.596667 |
| 2 | 11110515 | 11110515 | 2 | 103.07 | 175.205714 | 360.190000 |
| 3 | 11110515 | 11110515 | 3 | 16.30 | 239.946667 | 2441.596667 |
| 4 | 11110515 | 11110515 | 4 | 34.73 | 215.720000 | 860.990000 |
| ... | ... | ... | ... | ... | ... | ... |
| 3792913 | 52800410 | 41590360 | 11 | 2.56 | 69512.350000 | 4087.830000 |
| 3792914 | 52800420 | 11350570 | 14 | 3.50 | 66976.630000 | 6354.770000 |
| 3792915 | 52800420 | 11440630 | 9 | 3.48 | 63894.260000 | 3725.740000 |
| 3792916 | 52800420 | 41273610 | 17 | 2.33 | 69839.840000 | 1864.100000 |
| 3792917 | 52800420 | 41590253 | 15 | 3.72 | 50118.480000 | 3850.300000 |
3792918 rows × 6 columns
#### 출발 행정동 및 시간단위로 통행량 aggregation
df_agg = df.groupby(['O_ADMDONG_CD', 'ST_TIME_CD']).agg({'CNT': 'sum'}).reset_index()
df_agg
| O_ADMDONG_CD | ST_TIME_CD | CNT | |
|---|---|---|---|
| 0 | 11110515 | 0 | 61.31 |
| 1 | 11110515 | 1 | 133.12 |
| 2 | 11110515 | 2 | 150.33 |
| 3 | 11110515 | 3 | 64.69 |
| 4 | 11110515 | 4 | 87.28 |
| ... | ... | ... | ... |
| 53544 | 52800410 | 15 | 3.50 |
| 53545 | 52800420 | 9 | 3.48 |
| 53546 | 52800420 | 14 | 3.50 |
| 53547 | 52800420 | 15 | 3.72 |
| 53548 | 52800420 | 17 | 2.33 |
53549 rows × 3 columns
# 1. 먼저 pandas의 merge 함수를 사용하여 df_agg를 기준으로 병합
merged_df = df_agg.merge(simplified_geometry,
left_on='O_ADMDONG_CD',
right_on='ADMDONG_CD',
how='inner')
# 2. 병합 결과를 GeoDataFrame으로 변환
vis_gdf = gpd.GeoDataFrame(merged_df, geometry='geometry', crs=simplified_geometry.crs)
vis_gdf[vis_gdf['ST_TIME_CD'] == 1].explore(column='CNT',
legend=True,
cmap='Reds',
tiles='CartoDB positron',
style_kwds={'fillOpacity': 1, 'weight': 0.5})
2.2 Travel Time Data#
지금까지는 통행량 데이터를 살펴보았습니다. 이 장에서는 통행시간 데이터를 살펴봅니다.
스마트카드(지하철,버스), 택시 탑승 및 호출 이력, 스마트폰 GPS 데이터 등 다양한 경로를 통해 여러분들의 이동 데이터가 수집되고 있습니다.
그 중 택시 데이터는 승차와 하차의 정확한 위치와 시간을 알 수 있다는 점에서 데이터의 신뢰성이 높습니다.
다양한 국가에서 이미 표준화된 형태의 택시 승하차 데이터를 공개하고 있습니다.
본 장에서는 서울의 택시 승하차 이력 데이터를 살펴보겠습니다. 다만, 국내의 경우 택시승하차 데이터가 오픈되어있지 않으므로, 여기선 가상의 데이터를 활용합니다.
본 실습 데이터는 실제 데이터가 아니며, 분석을 위해 랜덤하게 생생된 1일치의 데이터입니다.
국내도 데이터 개방이 더 활성화 되어, 미국의 주요도시처럼 택시 및 스마트카드 데이터가 공개되는 날이 오기를 희망합니다.
2.2.1 데이터 읽기#
def download_and_read_parquet(file_id, output_path="../data/chp2_tx_data_generated.parquet"):
try:
# Google Drive에서 파일 다운로드
gdown.download(id=file_id, output=output_path, quiet=False)
# Parquet 파일을 DataFrame으로 읽기
df = pd.read_parquet(output_path)
# 임시 파일 삭제 (데이터 용량이 매우 큰 경우 사용)
# os.remove(output_path) # 다운로드 받은 데이터를 삭제하고 싶지 않을 때는 해당 라인을 주석처리
return df
except Exception as e:
print(f"오류 발생: {e}")
return None
# 파일 불러오기
file_id = "1uJj-C_7pTkcRk8p5lYah-9Fp660OvS4b" # 구글 드라이브에 업로드 된 파일의 ID
tx_data = download_and_read_parquet(file_id)
# tx_data = pd.read_parquet('../data/chp2_tx_data_generated.parquet')
Downloading...
From: https://drive.google.com/uc?id=1uJj-C_7pTkcRk8p5lYah-9Fp660OvS4b
To: f:\mobility-simulation-book\data\chp2_tx_data_generated.parquet
100%|██████████| 23.1M/23.1M [00:01<00:00, 21.2MB/s]
데이터의 시간 형식이 숫자형태로 되어있다. 이렇게 되어있을 경우 보기가 불편하고, 시간단위 연산이 어렵기 때문에 시간 타입의 데이터로 변환해주는 것이 좋다
# 시간 데이터 형식 변횐
tx_data['RIDE_DTIME'] = pd.to_datetime(tx_data['RIDE_DTIME'], format="%Y%m%d%H%M%S")
tx_data['ALIGHT_DTIME'] = pd.to_datetime(tx_data['ALIGHT_DTIME'], format="%Y%m%d%H%M%S")
# 통행시간 계산
tx_data['TRAVEL_TIME'] = (tx_data['ALIGHT_DTIME'] - tx_data['RIDE_DTIME']) / pd.Timedelta(minutes=1)
tx_data
| RIDE_DTIME | RIDE_POS_X | RIDE_POS_Y | ALIGHT_DTIME | ALIGHT_POS_X | ALIGHT_POS_Y | TRAVEL_TIME | |
|---|---|---|---|---|---|---|---|
| 0 | 2022-05-01 00:00:00 | 126.886907 | 37.482125 | 2022-05-01 00:07:04 | 126.896551 | 37.470549 | 7.066667 |
| 1 | 2022-05-01 00:00:00 | 126.960889 | 37.507369 | 2022-05-01 00:19:35 | 127.113011 | 37.503858 | 19.583333 |
| 2 | 2022-05-01 00:00:00 | 127.101762 | 37.467082 | 2022-05-01 00:13:08 | 127.052796 | 37.540526 | 13.133333 |
| 3 | 2022-05-01 00:00:00 | 127.055634 | 37.589778 | 2022-05-01 00:10:50 | 127.087957 | 37.597224 | 10.833333 |
| 4 | 2022-05-01 00:00:00 | 127.044648 | 37.567578 | 2022-05-01 00:03:57 | 127.037768 | 37.561673 | 3.950000 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 597090 | 2022-05-01 23:59:58 | 126.908084 | 37.515114 | 2022-05-02 00:30:52 | 126.781716 | 37.486141 | 30.900000 |
| 597091 | 2022-05-01 23:59:59 | 127.001369 | 37.558880 | 2022-05-02 00:14:48 | 127.034217 | 37.508548 | 14.816667 |
| 597092 | 2022-05-01 23:59:59 | 127.002711 | 37.505298 | 2022-05-02 00:05:59 | 127.011192 | 37.492397 | 6.000000 |
| 597093 | 2022-05-01 23:59:59 | 127.131851 | 37.535969 | 2022-05-02 00:09:24 | 127.160008 | 37.550329 | 9.416667 |
| 597094 | 2022-05-01 23:59:59 | 126.908992 | 37.518746 | 2022-05-02 00:17:20 | 126.876963 | 37.477816 | 17.350000 |
597095 rows × 7 columns
2.2.2 Basic Visualization#
통행량 데이터와 마찬가지로 간단한 시각화를 해보자.
다음과 같은 오픈소스를 활용한다면 매끄럽게 시각화가 가능하다.
데이터를 csv로 저장한 후, Kepler를 통해 시각화를 해보자.
통행시간의 경우 출발지-목적지(O-D) 단위로 통행시간을 시각화 하는 연습도 해보자.
O-D 단위의 시각화의 경우 무수히 많은 O-D pairs가 존재하기 때문에 시각적으로 표현하기가 매우 어렵다.
또한, 지금 데이터의 공간단위가 이전에 다뤘던 행정구역과 같이 합산된 공간단위가 아니라, 세밀한 위경도 좌표로 표현되어 있기 때문에 더 어렵다.
위와 같은 문제들을 어떻게 해결할 수 있을까?
tx_data.to_csv('../data/tx_data_generated.csv')
2.3 Road Network and Geometry#
2.3.1 Open Street Map 데이터 살펴보기#
오픈스트리트맵 한국에서는 대한민국의 OSM을 이용하는 상세한 방법에 대한 메뉴얼을 제공해주고 있다. 상세한 내용은 이곳을 참고하면 되며, 본 장에서는 한국의 OSM 데이터를 불러와서 OSMnx 패키지로 간략히 분석하고 시각화 하는 작업을 진행한다.
OSM을 이용해서 수도권 지역의 도로 데이터를 가져오고, 시각화 하는 작업을 수행한다
OSMnx 이용해서 그래프 분석하고, 복잡한 그래프를 간소화 하는 작업을 실습한다
[1] Open Street Map 데이터 취득하기 (.osm.pbf 파일)#
South Korea 부분의 .osm.pbf 버튼 클릭
여기서 다운로드 받은 .osm.pbf 파일은 본 챕터에서는 사용하지 않지만 향후 Vehicle Router를 만들 때 활용한다.
import networkx as nx
import osmnx as ox
import folium
ox.config(use_cache=True, log_console=True)
ox.__version__
C:\Users\rnt53\AppData\Local\Temp\ipykernel_35820\4097976119.py:5: FutureWarning: The `utils.config` function is deprecated and will be removed in the v2.0.0 release. Instead, use the `settings` module directly to configure a global setting's value. For example, `ox.settings.log_console=True`. See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123
ox.config(use_cache=True, log_console=True)
'1.9.4'
[2] 내가 원하는 지역 osm 지도를 그래프 형태로 표현#
본 실습에서는
OSMnx패키지를 사용하여 원하는 도시의 Graph를 Python으로 가져와서 분석하는 실습을 진행한다.
# get a graph for some city
# www.openstreetmap.org에서 검색 결과가 city-state-country 단위로 나와야 함
# # 서울특별시 전체 도로 네트워크 불러오기
# G = ox.graph_from_place('서울특별시, 대한민국', network_type='drive')
# osmnx 그래프 생성( osm 지도 다운로드 )
G = ox.graph_from_place('성남시, 경기도, 대한민국', network_type='drive')
network_type='drive'는 도로 네트워크(차량이 다닐 수 있는 도로)만을 가져오는 옵션이다. 따라서 보행자 도로, 자전거 도로, 또는 작은 골목 등은 포함되지 않을 수 있다. 전체적인 도로와 교통 네트워크를 모두 포함하려면 다른 network_type을 사용할 수 있다.
OSMnx에서 제공하는 네트워크 타입 옵션들은 다음과 같다.
‘all’: 모든 도로 네트워크를 가져온다 (자동차, 자전거, 보행자 도로 등 포함)
‘all_private’: 모든 도로 네트워크(사유지 도로 포함)
‘bike’: 자전거 네트워크만 가져온다
‘walk’: 보행자 네트워크만 가져온다
‘drive’: 차량이 다니는 도로만 가져온다
‘drive_service’: 서비스 도로(차량 진입 가능하지만 주요 도로는 아님)도 포함
전체 네트워크를 가져오려면 다음과 같이 사용할 수 있다.
G = ox.graph_from_place('성남시, 경기도, 대한민국', network_type='all')
## 간단한 시각화
# fig, ax = ox.plot_graph(G)
fig, ax = ox.plot_graph(
G,
figsize=(12, 12),
node_size=5,
edge_linewidth=0.5
)
하지만 위와 같이 시각화를 한다면, static map이기 때문에 원하는 특정 지역을 세밀하게 관찰하기가 어렵다.
Interactive Map을 사용하면 속도는 더 느리지만, 보다 상세히 그래프를 살펴볼 수 있다.
# 그래프의 중심점 계산
center_lat, center_lon = ox.geocode('성남시, 경기도, 대한민국')
# folium 맵 생성
m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='cartodbpositron')
# 엣지(도로) 추가
for u, v, data in G.edges(data=True):
locations = [(G.nodes[u]['y'], G.nodes[u]['x']), (G.nodes[v]['y'], G.nodes[v]['x'])]
# 엣지(street) 정보 생성
edge_info = f"도로명: {data.get('name', 'Unknown')}<br>" \
f"길이: {data.get('length', 0):.2f} m<br>" \
f"도로 유형: {data.get('highway', 'Unknown')}"
folium.PolyLine(
locations=locations,
weight=2,
color='blue',
opacity=0.7,
tooltip=edge_info
).add_to(m)
# 노드(교차점) 추가
for node, data in G.nodes(data=True):
# 노드에 연결된 엣지 수 계산
degree = G.degree(node)
# 노드 정보 생성
node_info = f"Node ID: {node}<br>" \
f"위도: {data['y']:.6f}<br>" \
f"경도: {data['x']:.6f}<br>" \
f"연결된 도로 수: {degree}"
folium.CircleMarker(
location=(data['y'], data['x']),
radius=3,
popup=node_info,
color='red',
fill=True,
fillColor='red',
tooltip=f"Node ID: {node}"
).add_to(m)
# 맵 저장
m.save("../data/Sungnam_road_network_vis.html")
한가지 이상한 점이 있다. 모든 Egde가 직선으로표현되어, 실제 Basemap에 있는 도로와 정확하게 매칭되지 않는다
Open Steet Map의 edge는 도로의 geometry 정보를 포함하고 있으며, 이 정보를 사용하면 실제도로와 유사하게 시각화가 가능하다
# folium 맵 생성 (CARTODB positron 베이스맵 사용)
m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='cartodbpositron')
# 엣지(도로) 추가
for u, v, data in G.edges(data=True):
if 'geometry' in data:
# 실제 도로 지오메트리 사용
geometry = data['geometry']
coordinates = list(geometry.coords)
locations = [(y, x) for x, y in coordinates]
else:
# 지오메트리 정보가 없는 경우 직선으로 표시
locations = [(G.nodes[u]['y'], G.nodes[u]['x']), (G.nodes[v]['y'], G.nodes[v]['x'])]
# 엣지(street) 정보 생성
edge_info = f"도로명: {data.get('name', 'Unknown')}<br>" \
f"길이: {data.get('length', 0):.2f} m<br>" \
f"도로 유형: {data.get('highway', 'Unknown')}"
folium.PolyLine(
locations=locations,
weight=2,
color='blue',
opacity=0.7,
tooltip=edge_info
).add_to(m)
# 노드(교차점) 추가
for node, data in G.nodes(data=True):
# 노드에 연결된 엣지 수 계산
degree = G.degree(node)
# 노드 정보 생성
node_info = f"Node ID: {node}<br>" \
f"위도: {data['y']:.6f}<br>" \
f"경도: {data['x']:.6f}<br>" \
f"연결된 도로 수: {degree}"
folium.CircleMarker(
location=(data['y'], data['x']),
radius=3,
popup=node_info,
color='red',
fill=True,
fillColor='red',
fillOpacity=0.7,
tooltip=f"Node ID: {node}"
).add_to(m)
# 맵 저장
m.save("../data/Sungnam_road_network_vis_with_geometry.html")
위 HTML 파일을 열어서 한번 살펴보자. Folium은 Leaflet.js 기반으로 HTML을 생성하고 이를 웹페이지로 렌더링한다.
지도를 그리거나 데이터를 시각화할 때 매번 HTML 파일을 업데이트하거나 렌더링해야 하기 때문에 시간이 더 소요될 수 있습니다.
데이터가 매우 크고, 수가 많을 경우 GPU 가속을 활용하는 시각화 패키지나 툴을 이용하는 것이 도움이 된다.
베이스 맵의 경우 https://www.mapbox.com/에서 직접 커스터마이즈 하면 된다.
import pydeck as pdk
import pandas as pd
# 엣지(도로) 데이터 생성
edges = []
for u, v, data in G.edges(data=True):
if 'geometry' in data:
geometry = data['geometry']
coordinates = list(geometry.coords)
locations = [(y, x) for x, y in coordinates]
else:
locations = [(G.nodes[u]['y'], G.nodes[u]['x']), (G.nodes[v]['y'], G.nodes[v]['x'])]
for i in range(len(locations) - 1):
edges.append({
'start_lat': locations[i][0],
'start_lon': locations[i][1],
'end_lat': locations[i+1][0],
'end_lon': locations[i+1][1],
'name': data.get('name', 'Unknown'),
'length': data.get('length', 0),
'highway': data.get('highway', 'Unknown')
})
edges_df = pd.DataFrame(edges)
# 노드(교차점) 데이터 생성
nodes = []
for node, data in G.nodes(data=True):
nodes.append({
'lat': data['y'],
'lon': data['x'],
'node_id': node,
'degree': G.degree(node)
})
nodes_df = pd.DataFrame(nodes)
# Pydeck의 LineLayer와 ScatterplotLayer 설정
edge_layer = pdk.Layer(
"LineLayer",
data=edges_df,
get_source_position='[start_lon, start_lat]',
get_target_position='[end_lon, end_lat]',
get_color=[0, 0, 255],
get_width=2,
pickable=True,
auto_highlight=True,
tooltip={
"html": "도로명: {name}<br>길이: {length} m<br>도로 유형: {highway}",
"style": {"color": "white"}
}
)
node_layer = pdk.Layer(
"ScatterplotLayer",
data=nodes_df,
get_position='[lon, lat]',
get_fill_color=[255, 0, 0],
get_radius=20,
pickable=True,
auto_highlight=True,
tooltip={
"html": "Node ID: {node_id}<br>연결된 도로 수: {degree}",
"style": {"color": "white"}
}
)
# Pydeck View 설정
view_state = pdk.ViewState(
latitude=center_lat,
longitude=center_lon,
zoom=12,
bearing=0,
pitch=0
)
# Pydeck 맵 렌더링
r = pdk.Deck(
layers=[edge_layer, node_layer],
initial_view_state=view_state
)
# 맵 저장
r.to_html("../data/Sungnam_road_network_vis_with_pydeck.html")
구글 코랩에서 실행하기